package parse

import (
	"fmt"
	"go/parser"
	"go/token"
	"log"
	"path"
	"strings"

	"github.com/dave/dst"
	"github.com/dave/dst/decorator"
	"github.com/gogf/gf/frame/g"
	"github.com/gookit/goutil/fsutil"

	"ast/util"
)

type Project struct {
	ProjectName     string
	ProjectRootPath string
	RootPackage     *Package
	DSTFiles        map[string]*File
	Packages        map[string]*Package
	FileSet         *token.FileSet
}

func NewProject(projectName string, projectRootPath string) *Project {
	p := &Project{
		ProjectName:     projectName,
		ProjectRootPath: projectRootPath,
		DSTFiles:        map[string]*File{},
		Packages:        map[string]*Package{},
		FileSet:         token.NewFileSet(),
	}
	p.RootPackage = p.NewPackage(nil, projectName)
	p.Packages[p.RootPackage.GlobalName] = p.RootPackage
	return p
}
func (p *Project) Parse(filePath string) {
	parentDir := fsutil.Dir(filePath)
	util.ProcessAny(filePath, parentDir, p.ProcessFile, p.ProcessDir, p.IsGoFile, p.IsSrcDir)
	p.ExtractFiles()
}
func (p *Project) NewPackage(parent *Package, importPath string) *Package {
	trimPrefix := strings.TrimPrefix(strings.ReplaceAll(importPath, p.ProjectName, ""), "/")
	return &Package{
		Classes:       map[string]*Class{},
		Functions:     map[string]*dst.FuncDecl{},
		Files:         map[string]*File{},
		ParentPackage: parent,
		GlobalName:    importPath,
		NameInProject: trimPrefix,
	}
}
func (p *Project) ProcessDir(dirName string, parentDir string) {
	dirImportPath := p.ImportPath(dirName)
	_, ex := p.Packages[dirImportPath]
	if !ex {
		parentDirInProject := p.ImportPath(parentDir)
		parentPkg := p.Packages[parentDirInProject]
		pkg := p.NewPackage(parentPkg, dirImportPath)
		parentPkg.ChildPackages = append(parentPkg.ChildPackages, pkg)
		p.Packages[dirImportPath] = pkg
	}
}

func (p *Project) ProcessFile(fileFullName string, content string, parentDir string) {
	astFile, err := decorator.ParseFile(p.FileSet, fileFullName, content, parser.DeclarationErrors|parser.ParseComments)
	if err != nil {
		panic(err)
	}

	file := &File{
		DSTFile:     astFile,
		FullPath:    fileFullName,
		ProjectPath: strings.ReplaceAll(fileFullName, p.ProjectRootPath, ""),
		Imports:     map[string]string{},
	}
	for _, spec := range astFile.Imports {
		if spec.Name != nil {
			file.Imports[spec.Name.Name] = spec.Path.Value
		} else {
			file.Imports[spec.Path.Value] = spec.Path.Value
		}
	}
	p.DSTFiles[file.ProjectPath] = file
	//检查对应的Package是否存在
	pkg, ex := p.Packages[p.ImportPath(parentDir)]
	if !ex {
		panic("adfasdf")
	}
	file.Package = pkg
	pkg.Files[file.ProjectPath] = file
}

func (p *Project) ImportPath(fullName string) string {
	return fsutil.Realpath(p.ProjectName + "/" + strings.ReplaceAll(fullName, p.ProjectRootPath, ""))
}

func (p *Project) ExtractFiles() {
	for _, astFile := range p.DSTFiles {
		pack := astFile.Package
		p.extractFile(pack, astFile)
	}
}

func (p *Project) extractFile(pack *Package, file *File) {
	for _, del := range file.DSTFile.Decls {
		switch realDel := del.(type) {
		case *dst.FuncDecl:
			p.mergeFuncDecl(pack, realDel)
		case *dst.GenDecl:
			p.mergeGenDecl(pack, realDel)
		}
	}
}

func (p *Project) mergeGenDecl(pack *Package, del *dst.GenDecl) {
	for _, spec := range del.Specs {
		switch realSpec := spec.(type) {
		case *dst.TypeSpec:
			p.mergeType(pack, realSpec, del.Tok)
		case *dst.ImportSpec:
			p.mergeImport(pack, realSpec)
		case *dst.ValueSpec:
			p.mergeVarConst(pack, realSpec, del.Tok)
		}
	}
}

func (p *Project) mergeVarConst(pack *Package, spec *dst.ValueSpec, tok token.Token) {

}

func (p *Project) mergeImport(pack *Package, spec *dst.ImportSpec) {

}

func (p *Project) mergeType(pack *Package, spec *dst.TypeSpec, tok token.Token) {
	if tok != token.TYPE {
		log.Fatalln("Type Token Not Match")
		return
	}

	expr := spec.Type
	structType, ok := expr.(*dst.StructType)
	if !ok {
		g.Log().Infof("Merge Type Skip ,EXPR %+v,SPEC %+v", expr, spec)
		return
	}
	cls, ok := pack.Classes[spec.Name.Name]
	if !ok {
		cls = newClass(pack, spec.Name.Name)
		pack.Classes[spec.Name.Name] = cls
	}
	for _, field := range structType.Fields.List {
		if len(field.Names) == 0 {
			selectorExpr, ok := field.Type.(*dst.SelectorExpr)
			if !ok {
				g.Log().Infof("Merge Type Skip Field ,Field %+v", field)
				continue
			}
			cls.Fields[selectorExpr.Sel.Name] = field
		} else {
			cls.Fields[field.Names[0].Name] = field
		}
	}
}
func (p *Project) mergeFuncDecl(pack *Package, realNode *dst.FuncDecl) {
	if realNode.Recv == nil {
		pack.Functions[realNode.Name.Name] = realNode
	} else {
		recvClsName := p.GetRecvName(realNode)
		if len(recvClsName) == 0 {
			return
		}
		cls, ex := pack.Classes[recvClsName]
		if !ex {
			cls = newClass(pack, recvClsName)
			pack.Classes[recvClsName] = cls
		}
		cls.Functions[realNode.Name.Name] = realNode
	}

}

func (p *Project) GetRecvName(del *dst.FuncDecl) string {
	if del.Recv == nil {
		return ""
	}
	if len(del.Recv.List) == 0 {
		return ""
	}
	receiver := del.Recv.List[0]
	startEx, ok := receiver.Type.(*dst.StarExpr)
	if !ok {
		g.Log().Infof("Skip,Receiver Must Be Pointer Type,Func %+v", del.Name.Name)
		return ""
	}
	ident, ok := startEx.X.(*dst.Ident)
	if !ok {
		log.Fatalln("Receiver Type Ident Not Right")
		return ""
	}
	recvClsName := ident.Name
	return recvClsName
}

func (p *Project) IsGoFile(name string) bool {
	return strings.HasSuffix(name, ".go")
}

func (p *Project) IsSrcDir(name string) bool {
	_, curName := path.Split(name)
	return !strings.HasPrefix(curName, ".")
}

func (p *Project) ExportedClassName(c *Class) string {
	sprintf := fsutil.Realpath(fmt.Sprintf("%s/%s/%s", p.ProjectName, c.Package.NameInProject, c.Name))
	return GetExportPath(sprintf)
}

func (p *Project) ExportedFuncNameWithClass(c *Class, pkg *Package, funcName string, useGlobal bool) string {
	name := ""
	if c != nil {
		name = c.Name
	}
	return p.ExportedFuncName(name, funcName, pkg, useGlobal)
}

func (p *Project) ExportedFuncName(recvName string, funcName string, pkg *Package, useGlobal bool) string {
	realpath := fsutil.Realpath(p.ProjectName + "/" + pkg.NameInProject + "/" + recvName)
	exportPath := GetExportPath(strings.TrimPrefix(realpath, "/"))
	funPath := fmt.Sprintf("%s_%s", exportPath, funcName)
	if useGlobal {
		funPath = p.ProjectName + "." + funPath
	}
	return funPath
}
